/*
 * Copyright 2011 The IEC61850bean Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.beanit.iec61850bean;

import com.beanit.asn1bean.ber.ReverseByteArrayOutputStream;
import com.beanit.asn1bean.ber.types.BerInteger;
import com.beanit.asn1bean.ber.types.BerNull;
import com.beanit.asn1bean.ber.types.string.BerGraphicString;
import com.beanit.asn1bean.ber.types.string.BerVisibleString;
import com.beanit.iec61850bean.internal.BerBoolean;
import com.beanit.iec61850bean.internal.mms.asn1.*;
import com.beanit.iec61850bean.internal.mms.asn1.DeleteNamedVariableListRequest.ListOfVariableListName;
import com.beanit.iec61850bean.internal.mms.asn1.GetNameListRequest.ObjectScope;
import com.beanit.iec61850bean.internal.mms.asn1.RejectPDU.RejectReason;
import com.beanit.iec61850bean.internal.mms.asn1.ServiceError.ErrorClass;
import com.beanit.iec61850bean.internal.mms.asn1.WriteRequest.ListOfData;
import com.beanit.josistack.AcseAssociation;
import com.beanit.josistack.ByteBufferInputStream;
import com.beanit.josistack.ClientAcseSap;
import com.beanit.josistack.DecodingException;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.text.ParseException;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * Represents an association/connection to an IEC 61850 MMS server. An instance of <code>
 * ClientAssociation</code> is obtained using <code>ClientSap</code>. An association object can be
 * used to execute the IEC 61850 ACSI services. Note that not all ACSI services have a corresponding
 * function in this API. For example all GetDirectory and GetDefinition services are covered by
 * <code>retrieveModel()</code>. The control services can be executed by using getDataValues and
 * setDataValues on the control objects in the data model.
 */
public final class ClientAssociation {

    private static final Integer16 version = new Integer16(new byte[]{(byte) 0x01, (byte) 0x01});
    private static final ParameterSupportOptions proposedParameterCbbBitString =
            new ParameterSupportOptions(new byte[]{0x03, 0x05, (byte) 0xf1, 0x00});
    private final ClientReceiver clientReceiver;
    private final BlockingQueue<MMSpdu> incomingResponses = new LinkedBlockingQueue<>();
    private final ReverseByteArrayOutputStream reverseOStream =
            new ReverseByteArrayOutputStream(500, true);
    ServerModel serverModel;
    private AcseAssociation acseAssociation = null;
    private int responseTimeout;

    private int invokeId = 0;
    private byte[] servicesSupported = null;

    private int negotiatedMaxPduSize;
    private ClientEventListener reportListener = null;

    private boolean closed = false;

    ClientAssociation(
            InetAddress address,
            int port,
            InetAddress localAddr,
            int localPort,
            String authenticationParameter,
            ClientAcseSap acseSap,
            int proposedMaxMmsPduSize,
            int proposedMaxServOutstandingCalling,
            int proposedMaxServOutstandingCalled,
            int proposedDataStructureNestingLevel,
            byte[] servicesSupportedCalling,
            int responseTimeout,
            int messageFragmentTimeout,
            ClientEventListener reportListener)
            throws IOException {

        this.responseTimeout = responseTimeout;

        acseSap.tSap.setMessageFragmentTimeout(messageFragmentTimeout);
        acseSap.tSap.setMessageTimeout(responseTimeout);

        negotiatedMaxPduSize = proposedMaxMmsPduSize;

        this.reportListener = reportListener;

        associate(
                address,
                port,
                localAddr,
                localPort,
                authenticationParameter,
                acseSap,
                proposedMaxMmsPduSize,
                proposedMaxServOutstandingCalling,
                proposedMaxServOutstandingCalled,
                proposedDataStructureNestingLevel,
                servicesSupportedCalling);

        acseAssociation.setMessageTimeout(0);

        clientReceiver = new ClientReceiver(negotiatedMaxPduSize);
        clientReceiver.start();
    }

    private static ServiceError mmsDataAccessErrorToServiceError(BerInteger dataAccessError) {

        switch (dataAccessError.value.intValue()) {
            case 1:
                return new ServiceError(
                        ServiceError.FAILED_DUE_TO_SERVER_CONSTRAINT, "MMS DataAccessError: hardware-fault");
            case 2:
                return new ServiceError(
                        ServiceError.INSTANCE_LOCKED_BY_OTHER_CLIENT,
                        "MMS DataAccessError: temporarily-unavailable");
            case 3:
                return new ServiceError(
                        ServiceError.ACCESS_VIOLATION, "MMS DataAccessError: object-access-denied");
            case 5:
                return new ServiceError(
                        ServiceError.PARAMETER_VALUE_INCONSISTENT, "MMS DataAccessError: invalid-address");
            case 7:
                return new ServiceError(
                        ServiceError.TYPE_CONFLICT, "MMS DataAccessError: type-inconsistent");
            case 10:
                return new ServiceError(
                        ServiceError.INSTANCE_NOT_AVAILABLE, "MMS DataAccessError: object-non-existent");
            case 11:
                return new ServiceError(
                        ServiceError.PARAMETER_VALUE_INCONSISTENT, "MMS DataAccessError: object-value-invalid");
            default:
                return new ServiceError(
                        ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                        "MMS DataAccessError: " + dataAccessError.value);
        }
    }

    private static void testForErrorResponse(MMSpdu mmsResponsePdu) throws ServiceError {
        if (mmsResponsePdu.getConfirmedErrorPDU() == null) {
            return;
        }

        ErrorClass errClass = mmsResponsePdu.getConfirmedErrorPDU().getServiceError().getErrorClass();

        if (errClass != null) {
            if (errClass.getAccess() != null) {
                if (errClass.getAccess().value.intValue() == 3) {
                    throw new ServiceError(
                            ServiceError.ACCESS_VIOLATION,
                            "MMS confirmed error: class: \"access\", error code: \"object-access-denied\"");
                } else if (errClass.getAccess().value.intValue() == 2) {

                    throw new ServiceError(
                            ServiceError.INSTANCE_NOT_AVAILABLE,
                            "MMS confirmed error: class: \"access\", error code: \"object-non-existent\"");
                }

            } else if (errClass.getFile() != null) {
                if (errClass.getFile().value.intValue() == 7) {

                    throw new ServiceError(
                            ServiceError.FILE_NONE_EXISTENT,
                            "MMS confirmed error: class: \"file\", error code: \"file-non-existent\"");
                }
            }
        }

        if (mmsResponsePdu.getConfirmedErrorPDU().getServiceError().getAdditionalDescription()
                != null) {
            throw new ServiceError(
                    ServiceError.UNKNOWN,
                    "MMS confirmed error. Description: "
                            + mmsResponsePdu
                            .getConfirmedErrorPDU()
                            .getServiceError()
                            .getAdditionalDescription()
                            .toString());
        }
        throw new ServiceError(ServiceError.UNKNOWN, "MMS confirmed error.");
    }

    private static void testForRejectResponse(MMSpdu mmsResponsePdu) throws ServiceError {
        if (mmsResponsePdu.getRejectPDU() == null) {
            return;
        }

        RejectReason rejectReason = mmsResponsePdu.getRejectPDU().getRejectReason();
        if (rejectReason != null) {
            if (rejectReason.getPduError() != null) {
                if (rejectReason.getPduError().value.intValue() == 1) {
                    throw new ServiceError(
                            ServiceError.PARAMETER_VALUE_INCONSISTENT,
                            "MMS reject: type: \"pdu-error\", reject code: \"invalid-pdu\"");
                }
            }
        }
        throw new ServiceError(ServiceError.UNKNOWN, "MMS confirmed error.");
    }

    private static void testForInitiateErrorResponse(MMSpdu mmsResponsePdu) throws ServiceError {
        if (mmsResponsePdu.getInitiateErrorPDU() != null) {

            ErrorClass errClass = mmsResponsePdu.getInitiateErrorPDU().getErrorClass();
            if (errClass != null) {
                if (errClass.getVmdState() != null) {
                    throw new ServiceError(
                            ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "error class \"vmd_state\" with val: " + errClass.getVmdState().value);
                }
                if (errClass.getApplicationReference() != null) {
                    throw new ServiceError(
                            ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "error class \"application_reference\" with val: "
                                    + errClass.getApplicationReference().value);
                }
                if (errClass.getDefinition() != null) {
                    throw new ServiceError(
                            ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "error class \"definition\" with val: " + errClass.getDefinition().value);
                }
                if (errClass.getResource() != null) {
                    throw new ServiceError(
                            ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "error class \"resource\" with val: " + errClass.getResource().value);
                }
                if (errClass.getService() != null) {
                    throw new ServiceError(
                            ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "error class \"service\" with val: " + errClass.getService().value);
                }
                if (errClass.getServicePreempt() != null) {
                    throw new ServiceError(
                            ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "error class \"service_preempt\" with val: " + errClass.getServicePreempt().value);
                }
                if (errClass.getTimeResolution() != null) {
                    throw new ServiceError(
                            ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "error class \"time_resolution\" with val: " + errClass.getTimeResolution().value);
                }
                if (errClass.getAccess() != null) {
                    throw new ServiceError(
                            ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "error class \"access\" with val: " + errClass.getAccess().value);
                }
                if (errClass.getInitiate() != null) {
                    throw new ServiceError(
                            ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "error class \"initiate\" with val: " + errClass.getInitiate().value);
                }
                if (errClass.getConclude() != null) {
                    throw new ServiceError(
                            ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "error class \"conclude\" with val: " + errClass.getConclude());
                }
                if (errClass.getCancel() != null) {
                    throw new ServiceError(
                            ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "error class \"cancel\" with val: " + errClass.getCancel().value);
                }
                if (errClass.getFile() != null) {
                    throw new ServiceError(
                            ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "error class \"file\" with val: " + errClass.getFile().value);
                }
                if (errClass.getOthers() != null) {
                    throw new ServiceError(
                            ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "error class \"others\" with val: " + errClass.getOthers().value);
                }
            }

            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT, "unknown error class");
        }
    }

    private static MMSpdu constructInitRequestPdu(
            int proposedMaxPduSize,
            int proposedMaxServOutstandingCalling,
            int proposedMaxServOutstandingCalled,
            int proposedDataStructureNestingLevel,
            byte[] servicesSupportedCalling) {

        InitiateRequestPDU.InitRequestDetail initRequestDetail =
                new InitiateRequestPDU.InitRequestDetail();
        initRequestDetail.setProposedVersionNumber(version);
        initRequestDetail.setProposedParameterCBB(proposedParameterCbbBitString);
        initRequestDetail.setServicesSupportedCalling(
                new ServiceSupportOptions(servicesSupportedCalling, 85));

        InitiateRequestPDU initiateRequestPdu = new InitiateRequestPDU();
        initiateRequestPdu.setLocalDetailCalling(new Integer32(proposedMaxPduSize));
        initiateRequestPdu.setProposedMaxServOutstandingCalling(
                new Integer16(proposedMaxServOutstandingCalling));
        initiateRequestPdu.setProposedMaxServOutstandingCalled(
                new Integer16(proposedMaxServOutstandingCalled));
        initiateRequestPdu.setProposedDataStructureNestingLevel(
                new Integer8(proposedDataStructureNestingLevel));
        initiateRequestPdu.setInitRequestDetail(initRequestDetail);

        MMSpdu initiateRequestMMSpdu = new MMSpdu();
        initiateRequestMMSpdu.setInitiateRequestPDU(initiateRequestPdu);

        return initiateRequestMMSpdu;
    }

    /**
     * Gets the response timeout. The response timeout is used whenever a request is sent to the
     * server. The client will wait for this amount of time for the server's response before throwing
     * a ServiceError.TIMEOUT. Responses received after the timeout will be automatically discarded.
     *
     * @return the response timeout in milliseconds.
     */
    public int getResponseTimeout() {
        return responseTimeout;
    }

    /**
     * Sets the response timeout. The response timeout is used whenever a request is sent to the
     * server. The client will wait for this amount of time for the server's response before throwing
     * a ServiceError.TIMEOUT. Responses received after the timeout will be automatically discarded.
     *
     * @param timeout the response timeout in milliseconds.
     */
    public void setResponseTimeout(int timeout) {
        responseTimeout = timeout;
    }

    private int getInvokeId() {
        invokeId = (invokeId + 1) % 2147483647;
        return invokeId;
    }

    /**
     * Gets the ServicesSupported. The 11-byte array of bit flags is available after InitiateResponse
     * is received from the server, otherwise null is returned
     *
     * @return the ServicesSupported byte array, or null.
     */
    public byte[] getServicesSupported() {
        return servicesSupported;
    }

    private ConfirmedServiceResponse encodeWriteReadDecode(ConfirmedServiceRequest serviceRequest)
            throws ServiceError, IOException {

        int currentInvokeId = getInvokeId();

        ConfirmedRequestPDU confirmedRequestPdu = new ConfirmedRequestPDU();
        confirmedRequestPdu.setInvokeID(new Unsigned32(currentInvokeId));
        confirmedRequestPdu.setService(serviceRequest);

        MMSpdu requestPdu = new MMSpdu();
        requestPdu.setConfirmedRequestPDU(confirmedRequestPdu);

        reverseOStream.reset();

        try {
            requestPdu.encode(reverseOStream);
        } catch (Exception e) {
            IOException e2 = new IOException("Error encoding MmsPdu.", e);
            clientReceiver.close(e2);
            throw e2;
        }

        clientReceiver.setResponseExpected(currentInvokeId);
        try {
            acseAssociation.send(reverseOStream.getByteBuffer());
        } catch (IOException e) {
            IOException e2 = new IOException("Error sending packet.", e);
            clientReceiver.close(e2);
            throw e2;
        }

        MMSpdu decodedResponsePdu = null;

        try {
            if (responseTimeout == 0) {
                decodedResponsePdu = incomingResponses.take();
            } else {
                decodedResponsePdu = incomingResponses.poll(responseTimeout, TimeUnit.MILLISECONDS);
            }
        } catch (InterruptedException e) {
            // TODO can this ever be interrupted?
        }

        if (decodedResponsePdu == null) {
            decodedResponsePdu = clientReceiver.removeExpectedResponse();
            if (decodedResponsePdu == null) {
                throw new ServiceError(ServiceError.TIMEOUT);
            }
        }

        if (decodedResponsePdu.getConfirmedRequestPDU() != null) {
            incomingResponses.add(decodedResponsePdu);
            throw new IOException("connection was closed", clientReceiver.getLastIOException());
        }

        testForInitiateErrorResponse(decodedResponsePdu);
        testForErrorResponse(decodedResponsePdu);
        testForRejectResponse(decodedResponsePdu);

        ConfirmedResponsePDU confirmedResponsePdu = decodedResponsePdu.getConfirmedResponsePDU();
        if (confirmedResponsePdu == null) {
            throw new IllegalStateException("Response PDU is not a confirmed response pdu");
        }

        return confirmedResponsePdu.getService();
    }

    private void associate(
            InetAddress address,
            int port,
            InetAddress localAddr,
            int localPort,
            String authenticationParameter,
            ClientAcseSap acseSap,
            int proposedMaxPduSize,
            int proposedMaxServOutstandingCalling,
            int proposedMaxServOutstandingCalled,
            int proposedDataStructureNestingLevel,
            byte[] servicesSupportedCalling)
            throws IOException {

        MMSpdu initiateRequestMMSpdu =
                constructInitRequestPdu(
                        proposedMaxPduSize,
                        proposedMaxServOutstandingCalling,
                        proposedMaxServOutstandingCalled,
                        proposedDataStructureNestingLevel,
                        servicesSupportedCalling);

        ReverseByteArrayOutputStream reverseOStream = new ReverseByteArrayOutputStream(500, true);
        initiateRequestMMSpdu.encode(reverseOStream);

        try {
            acseAssociation =
                    acseSap.associate(
                            address,
                            port,
                            localAddr,
                            localPort,
                            authenticationParameter,
                            reverseOStream.getByteBuffer());

            ByteBuffer initResponse = acseAssociation.getAssociateResponseAPdu();

            MMSpdu initiateResponseMmsPdu = new MMSpdu();

            initiateResponseMmsPdu.decode(new ByteBufferInputStream(initResponse), null);

            handleInitiateResponse(
                    initiateResponseMmsPdu,
                    proposedMaxPduSize,
                    proposedMaxServOutstandingCalling,
                    proposedMaxServOutstandingCalled,
                    proposedDataStructureNestingLevel);
        } catch (IOException e) {
            if (acseAssociation != null) {
                acseAssociation.close();
            }
            throw e;
        }
    }

    private void handleInitiateResponse(
            MMSpdu responsePdu,
            int proposedMaxPduSize,
            int proposedMaxServOutstandingCalling,
            int proposedMaxServOutstandingCalled,
            int proposedDataStructureNestingLevel)
            throws IOException {

        if (responsePdu.getInitiateErrorPDU() != null) {
            throw new IOException(
                    "Got response error of class: " + responsePdu.getInitiateErrorPDU().getErrorClass());
        }

        if (responsePdu.getInitiateResponsePDU() == null) {
            acseAssociation.disconnect();
            throw new IOException("Error decoding InitiateResponse Pdu");
        }

        InitiateResponsePDU initiateResponsePdu = responsePdu.getInitiateResponsePDU();

        if (initiateResponsePdu.getLocalDetailCalled() != null) {
            negotiatedMaxPduSize = initiateResponsePdu.getLocalDetailCalled().intValue();
        }

        int negotiatedMaxServOutstandingCalling =
                initiateResponsePdu.getNegotiatedMaxServOutstandingCalling().intValue();
        int negotiatedMaxServOutstandingCalled =
                initiateResponsePdu.getNegotiatedMaxServOutstandingCalled().intValue();

        int negotiatedDataStructureNestingLevel;
        if (initiateResponsePdu.getNegotiatedDataStructureNestingLevel() != null) {
            negotiatedDataStructureNestingLevel =
                    initiateResponsePdu.getNegotiatedDataStructureNestingLevel().intValue();
        } else {
            negotiatedDataStructureNestingLevel = proposedDataStructureNestingLevel;
        }

        if (negotiatedMaxPduSize < ClientSap.MINIMUM_MMS_PDU_SIZE
                || negotiatedMaxPduSize > proposedMaxPduSize
                || negotiatedMaxServOutstandingCalling > proposedMaxServOutstandingCalling
                || negotiatedMaxServOutstandingCalling < 0
                || negotiatedMaxServOutstandingCalled > proposedMaxServOutstandingCalled
                || negotiatedMaxServOutstandingCalled < 0
                || negotiatedDataStructureNestingLevel > proposedDataStructureNestingLevel
                || negotiatedDataStructureNestingLevel < 0) {
            acseAssociation.disconnect();
            throw new IOException("Error negotiating parameters");
        }

        int version =
                initiateResponsePdu.getInitResponseDetail().getNegotiatedVersionNumber().intValue();
        if (version != 1) {
            throw new IOException("Unsupported version number was negotiated.");
        }

        servicesSupported =
                initiateResponsePdu.getInitResponseDetail().getServicesSupportedCalled().value;
        if ((servicesSupported[0] & 0x40) != 0x40) {
            throw new IOException("Obligatory services are not supported by the server.");
        }
    }

    /**
     * Set the server model instead of retrieving it from the server device.
     *
     * @param model the server model
     */
    public void setServerModel(ServerModel model) {
        this.serverModel = model;
    }

    /**
     * Triggers all GetDirectory and GetDefinition ACSI services needed to get the complete server
     * model. Because in MMS SubDataObjects cannot be distinguished from Constructed Data Attributes
     * they will always be represented as Constructed Data Attributes in the returned model.
     *
     * @return the ServerModel that is the root node of the complete server model.
     * @throws ServiceError if a ServiceError occurs while calling any of the ASCI services.
     * @throws IOException  if a fatal association error occurs. The association object will be closed
     *                      and can no longer be used after this exception is thrown.
     */
    public ServerModel retrieveModel() throws ServiceError, IOException {

        List<String> ldNames = retrieveLogicalDevices();
        List<List<String>> lnNames = new ArrayList<>(ldNames.size());

        for (int i = 0; i < ldNames.size(); i++) {
            lnNames.add(retrieveLogicalNodeNames(ldNames.get(i)));
        }
        List<LogicalDevice> lds = new ArrayList<>();
        for (int i = 0; i < ldNames.size(); i++) {
            List<LogicalNode> lns = new ArrayList<>();
            for (int j = 0; j < lnNames.get(i).size(); j++) {
                lns.add(
                        retrieveDataDefinitions(
                                new ObjectReference(ldNames.get(i) + "/" + lnNames.get(i).get(j))));
            }
            lds.add(new LogicalDevice(new ObjectReference(ldNames.get(i)), lns));
        }

        serverModel = new ServerModel(lds, null);

        updateDataSets();

        return serverModel;
    }

    private List<String> retrieveLogicalDevices() throws ServiceError, IOException {
        ConfirmedServiceRequest serviceRequest = constructGetServerDirectoryRequest();
        ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
        return decodeGetServerDirectoryResponse(confirmedServiceResponse);
    }

    private ConfirmedServiceRequest constructGetServerDirectoryRequest() {
        ObjectClass objectClass = new ObjectClass();
        objectClass.setBasicObjectClass(new BerInteger(9));

        GetNameListRequest.ObjectScope objectScope = new GetNameListRequest.ObjectScope();
        objectScope.setVmdSpecific(new BerNull());

        GetNameListRequest getNameListRequest = new GetNameListRequest();
        getNameListRequest.setObjectClass(objectClass);
        getNameListRequest.setObjectScope(objectScope);

        ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
        confirmedServiceRequest.setGetNameList(getNameListRequest);

        return confirmedServiceRequest;
    }

    private List<String> decodeGetServerDirectoryResponse(
            ConfirmedServiceResponse confirmedServiceResponse) throws ServiceError {

        if (confirmedServiceResponse.getGetNameList() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "Error decoding Get Server Directory Response Pdu");
        }

        List<Identifier> identifiers =
                confirmedServiceResponse.getGetNameList().getListOfIdentifier().getIdentifier();
        ArrayList<String> objectRefs = new ArrayList<>(); // ObjectReference[identifiers.size()];

        for (BerVisibleString identifier : identifiers) {
            objectRefs.add(identifier.toString());
        }

        return objectRefs;
    }

    private List<String> retrieveLogicalNodeNames(String ld) throws ServiceError, IOException {
        List<String> lns = new ArrayList<>();
        String continueAfterRef = "";
        do {
            ConfirmedServiceRequest serviceRequest =
                    constructGetDirectoryRequest(ld, continueAfterRef, true);
            ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
            continueAfterRef = decodeGetDirectoryResponse(confirmedServiceResponse, lns);

        } while (!continueAfterRef.isEmpty());
        return lns;
    }

    private ConfirmedServiceRequest constructGetDirectoryRequest(
            String ldRef, String continueAfter, boolean logicalDevice) {

        ObjectClass objectClass = new ObjectClass();

        if (logicalDevice) {
            objectClass.setBasicObjectClass(new BerInteger(0));
        } else { // for data sets
            objectClass.setBasicObjectClass(new BerInteger(2));
        }

        GetNameListRequest getNameListRequest;

        ObjectScope objectScopeChoiceType = new ObjectScope();
        objectScopeChoiceType.setDomainSpecific(new Identifier(ldRef.getBytes(UTF_8)));

        getNameListRequest = new GetNameListRequest();
        getNameListRequest.setObjectClass(objectClass);
        getNameListRequest.setObjectScope(objectScopeChoiceType);
        if (!continueAfter.isEmpty()) {
            getNameListRequest.setContinueAfter(new Identifier(continueAfter.getBytes(UTF_8)));
        }

        ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
        confirmedServiceRequest.setGetNameList(getNameListRequest);
        return confirmedServiceRequest;
    }

    /**
     * Decodes an MMS response which contains the structure of a LD and its LNs including names of
     * DOs.
     */
    private String decodeGetDirectoryResponse(
            ConfirmedServiceResponse confirmedServiceResponse, List<String> lns) throws ServiceError {

        if (confirmedServiceResponse.getGetNameList() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "decodeGetLDDirectoryResponse: Error decoding server response");
        }

        GetNameListResponse getNameListResponse = confirmedServiceResponse.getGetNameList();

        List<Identifier> identifiers = getNameListResponse.getListOfIdentifier().getIdentifier();

        if (identifiers.size() == 0) {
            throw new ServiceError(
                    ServiceError.INSTANCE_NOT_AVAILABLE,
                    "decodeGetLDDirectoryResponse: Instance not available");
        }

        BerVisibleString identifier = null;
        Iterator<Identifier> it = identifiers.iterator();

        String idString;

        while (it.hasNext()) {
            identifier = it.next();
            idString = identifier.toString();

            if (idString.indexOf('$') == -1) {
                lns.add(idString);
            }
        }

        if (getNameListResponse.getMoreFollows() != null
                && getNameListResponse.getMoreFollows().value == false) {
            return "";
        } else {
            return identifier.toString();
        }
    }

    private LogicalNode retrieveDataDefinitions(ObjectReference lnRef)
            throws ServiceError, IOException {
        ConfirmedServiceRequest serviceRequest = constructGetDataDefinitionRequest(lnRef);
        ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
        return decodeGetDataDefinitionResponse(confirmedServiceResponse, lnRef);
    }

    private ConfirmedServiceRequest constructGetDataDefinitionRequest(ObjectReference lnRef) {

        ObjectName.DomainSpecific domainSpec = new ObjectName.DomainSpecific();
        domainSpec.setDomainID(new Identifier(lnRef.get(0).getBytes(UTF_8)));
        domainSpec.setItemID(new Identifier(lnRef.get(1).getBytes(UTF_8)));

        ObjectName objectName = new ObjectName();
        objectName.setDomainSpecific(domainSpec);

        GetVariableAccessAttributesRequest getVariableAccessAttributesRequest =
                new GetVariableAccessAttributesRequest();
        getVariableAccessAttributesRequest.setName(objectName);

        ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
        confirmedServiceRequest.setGetVariableAccessAttributes(getVariableAccessAttributesRequest);

        return confirmedServiceRequest;
    }

    private LogicalNode decodeGetDataDefinitionResponse(
            ConfirmedServiceResponse confirmedServiceResponse, ObjectReference lnRef)
            throws ServiceError {

        return DataDefinitionResParser.parseGetDataDefinitionResponse(confirmedServiceResponse, lnRef);
    }

    /**
     * The implementation of the GetDataValues ACSI service. Will send an MMS read request for the
     * given model node. After a successful return, the Basic Data Attributes of the passed model node
     * will contain the values read. If one of the Basic Data Attributes cannot be read then none of
     * the values will be read and a <code>ServiceError</code> will be thrown.
     *
     * @param modelNode the functionally constrained model node that is to be read.
     * @throws ServiceError if a ServiceError is returned by the server.
     * @throws IOException  if a fatal association error occurs. The association object will be closed
     *                      and can no longer be used after this exception is thrown.
     */
    public void getDataValues(FcModelNode modelNode) throws ServiceError, IOException {
        ConfirmedServiceRequest serviceRequest = constructGetDataValuesRequest(modelNode);
        ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
        decodeGetDataValuesResponse(confirmedServiceResponse, modelNode);
    }

    private boolean decodeGetFileDirectoryResponse(
            ConfirmedServiceResponse confirmedServiceResponse, List<FileInformation> files)
            throws ServiceError {
        if (confirmedServiceResponse.getFileDirectory() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "Error decoding GetFileDirectoryResponsePdu");
        }

        FileDirectoryResponse fileDirectoryRes = confirmedServiceResponse.getFileDirectory();

        List<DirectoryEntry> entries = fileDirectoryRes.getListOfDirectoryEntry().getDirectoryEntry();

        for (DirectoryEntry entry : entries) {
            List<BerGraphicString> graphicStrings = entry.getFileName().getBerGraphicString();

            StringBuilder filename = new StringBuilder();

            for (BerGraphicString bgs : graphicStrings) {
                filename.append(bgs.toString());
            }

            long fileSize = entry.getFileAttributes().getSizeOfFile().longValue();

            Calendar lastModified = null;

            try {

                if (entry.getFileAttributes().getLastModified() != null) {
                    lastModified = entry.getFileAttributes().getLastModified().asCalendar();
                }

            } catch (ParseException e) {
                throw new ServiceError(
                        ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                        "Error decoding GetFileDirectoryResponsePdu");
            }

            FileInformation fileInfo = new FileInformation(filename.toString(), fileSize, lastModified);

            files.add(fileInfo);
        }

        boolean moreFollows =
                (fileDirectoryRes.getMoreFollows() != null) && fileDirectoryRes.getMoreFollows().value;

        return moreFollows;
    }

    /**
     * Read the file directory of the server
     *
     * @param directoryName name of a directory or empty string for the root directory
     * @return the list of available
     * @throws ServiceError if a ServiceError is returned by the server or parsing of response failed.
     * @throws IOException  if a fatal association error occurs. The association object will be closed
     *                      and can no longer be used after this exception is thrown.
     */
    public List<FileInformation> getFileDirectory(String directoryName)
            throws ServiceError, IOException {
        List<FileInformation> files = new ArrayList<>();

        boolean moreFollows = true;

        String continueAfter = null;

        while (moreFollows) {

            FileDirectoryRequest fileDirectoryRequest = new FileDirectoryRequest();

            BerGraphicString berGraphicString = new BerGraphicString(directoryName.getBytes(UTF_8));

            FileName fileSpecification = new FileName();
            fileSpecification.getBerGraphicString().add(berGraphicString);

            fileDirectoryRequest.setFileSpecification(fileSpecification);

            if (continueAfter != null) {
                FileName continueAfterSpecification = new FileName();

                continueAfterSpecification
                        .getBerGraphicString()
                        .add(new BerGraphicString(continueAfter.getBytes(UTF_8)));

                fileDirectoryRequest.setContinueAfter(continueAfterSpecification);
            }

            ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
            confirmedServiceRequest.setFileDirectory(fileDirectoryRequest);

            ConfirmedServiceResponse confirmedServiceResponse =
                    encodeWriteReadDecode(confirmedServiceRequest);

            moreFollows = decodeGetFileDirectoryResponse(confirmedServiceResponse, files);

            if (moreFollows) {
                continueAfter = files.get(files.size() - 1).getFilename();
            }
        }

        return files;
    }

    /**
     * Delete a file from the server
     *
     * @param filename name of the file to delete
     * @throws ServiceError if a ServiceError is returned by the server
     * @throws IOException  if a fatal association error occurs. The association object will be closed
     *                      and can no longer be used after this exception is thrown.
     */
    public void deleteFile(String filename) throws ServiceError, IOException {
        FileDeleteRequest fileDeleteRequest = new FileDeleteRequest();

        fileDeleteRequest.getBerGraphicString().add(new BerGraphicString(filename.getBytes(UTF_8)));

        ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
        confirmedServiceRequest.setFileDelete(fileDeleteRequest);

        ConfirmedServiceResponse confirmedServiceResponse =
                encodeWriteReadDecode(confirmedServiceRequest);

        if (confirmedServiceResponse.getFileDelete() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "Error decoding DeleteFileResponsePdu");
        }
    }

    private Integer32 openFile(String filename) throws ServiceError, IOException {
        FileOpenRequest fileOpenRequest = new FileOpenRequest();

        FileName fileSpecification = new FileName();
        fileSpecification.getBerGraphicString().add(new BerGraphicString(filename.getBytes(UTF_8)));

        fileOpenRequest.setFileName(fileSpecification);
        fileOpenRequest.setInitialPosition(new Unsigned32(0));

        ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
        confirmedServiceRequest.setFileOpen(fileOpenRequest);

        ConfirmedServiceResponse confirmedServiceResponse =
                encodeWriteReadDecode(confirmedServiceRequest);

        if (confirmedServiceResponse.getFileOpen() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "Error decoding FileOpenResponsePdu");
        }

        Integer32 frsmId = confirmedServiceResponse.getFileOpen().getFrsmID();

        return frsmId;
    }

    private boolean readNextFileDataBlock(Integer32 frsmId, GetFileListener listener)
            throws ServiceError, IOException {
        FileReadRequest fileReadRequest = new FileReadRequest(frsmId.longValue());

        ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
        confirmedServiceRequest.setFileRead(fileReadRequest);

        ConfirmedServiceResponse confirmedServiceResponse =
                encodeWriteReadDecode(confirmedServiceRequest);

        if (confirmedServiceResponse.getFileRead() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "Error decoding FileReadResponsePdu");
        }

        byte[] fileData = confirmedServiceResponse.getFileRead().getFileData().value;

        boolean moreFollows = true;

        if (confirmedServiceResponse.getFileRead().getMoreFollows() != null) {
            moreFollows = confirmedServiceResponse.getFileRead().getMoreFollows().value;
        }

        if (listener != null) {
            boolean continueRead = listener.dataReceived(fileData, moreFollows);

            if (moreFollows == true) {
                moreFollows = continueRead;
            }
        }

        return moreFollows;
    }

    private void closeFile(Integer32 frsmId) throws ServiceError, IOException {
        FileCloseRequest fileCloseRequest = new FileCloseRequest(frsmId.longValue());

        ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
        confirmedServiceRequest.setFileClose(fileCloseRequest);

        ConfirmedServiceResponse confirmedServiceResponse =
                encodeWriteReadDecode(confirmedServiceRequest);

        if (confirmedServiceResponse.getFileClose() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "Error decoding FileCloseResponsePdu");
        }
    }

    /**
     * Read a file from the server
     *
     * @param filename name of the file to delete
     * @param listener callback handler to receive fall data
     * @throws ServiceError if a ServiceError is returned by the server
     * @throws IOException  if a fatal association error occurs. The association object will be closed
     *                      and can no longer be used after this exception is thrown.
     */
    public void getFile(String filename, GetFileListener listener) throws ServiceError, IOException {
        Integer32 frsmId = openFile(filename);

        boolean moreFollows = true;

        while (moreFollows) {
            moreFollows = readNextFileDataBlock(frsmId, listener);
        }

        closeFile(frsmId);
    }

    /**
     * Will update all data inside the model except for control variables (those that have FC=CO).
     * Control variables are not meant to be read. Update is done by calling getDataValues on the
     * FCDOs below the Logical Nodes.
     *
     * @throws ServiceError if a ServiceError is returned by the server.
     * @throws IOException  if a fatal association error occurs. The association object will be closed
     *                      and can no longer be used after this exception is thrown.
     */
    public void getAllDataValues() throws ServiceError, IOException {
        for (ModelNode logicalDevice : serverModel.getChildren()) {
            for (ModelNode logicalNode : logicalDevice.getChildren()) {
                for (ModelNode dataObject : logicalNode.getChildren()) {
                    FcModelNode fcdo = (FcModelNode) dataObject;
                    if (fcdo.getFc() != Fc.CO && fcdo.getFc() != Fc.SE) {
                        try {
                            getDataValues(fcdo);
                        } catch (ServiceError e) {
                            throw new ServiceError(
                                    e.getErrorCode(),
                                    "service error retrieving "
                                            + fcdo.getReference()
                                            + "["
                                            + fcdo.getFc()
                                            + "]"
                                            + ", "
                                            + e.getMessage(),
                                    e);
                        }
                    }
                }
            }
        }
    }

    private ConfirmedServiceRequest constructGetDataValuesRequest(FcModelNode modelNode) {
        VariableAccessSpecification varAccessSpec = constructVariableAccessSpecification(modelNode);

        ReadRequest readRequest = new ReadRequest();
        readRequest.setVariableAccessSpecification(varAccessSpec);

        ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
        confirmedServiceRequest.setRead(readRequest);

        return confirmedServiceRequest;
    }

    private void decodeGetDataValuesResponse(
            ConfirmedServiceResponse confirmedServiceResponse, ModelNode modelNode) throws ServiceError {

        if (confirmedServiceResponse.getRead() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "Error decoding GetDataValuesReponsePdu");
        }

        List<AccessResult> listOfAccessResults =
                confirmedServiceResponse.getRead().getListOfAccessResult().getAccessResult();

        if (listOfAccessResults.size() != 1) {
            throw new ServiceError(
                    ServiceError.PARAMETER_VALUE_INAPPROPRIATE, "Multiple results received.");
        }

        AccessResult accRes = listOfAccessResults.get(0);

        if (accRes.getFailure() != null) {
            throw mmsDataAccessErrorToServiceError(accRes.getFailure());
        }
        modelNode.setValueFromMmsDataObj(accRes.getSuccess());
    }

    /**
     * The implementation of the SetDataValues ACSI service. Will send an MMS write request with the
     * values of all Basic Data Attributes of the given model node. Will simply return if all values
     * have been successfully written. If one of the Basic Data Attributes could not be written then a
     * <code>ServiceError</code> will be thrown. In this case it is not possible to find out which of
     * several Basic Data Attributes could not be written.
     *
     * @param modelNode the functionally constrained model node that is to be written.
     * @throws ServiceError if a ServiceError is returned by the server.
     * @throws IOException  if a fatal association error occurs. The association object will be closed
     *                      and can no longer be used after this exception is thrown.
     */
    public void setDataValues(FcModelNode modelNode) throws ServiceError, IOException {
        ConfirmedServiceRequest serviceRequest = constructSetDataValuesRequest(modelNode);
        ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
        decodeSetDataValuesResponse(confirmedServiceResponse);
    }

    private ConfirmedServiceRequest constructSetDataValuesRequest(FcModelNode modelNode)
            throws ServiceError {

        VariableAccessSpecification variableAccessSpecification =
                constructVariableAccessSpecification(modelNode);

        ListOfData listOfData = new ListOfData();
        List<Data> dataList = listOfData.getData();
        dataList.add(modelNode.getMmsDataObj());

        WriteRequest writeRequest = new WriteRequest();
        writeRequest.setListOfData(listOfData);
        writeRequest.setVariableAccessSpecification(variableAccessSpecification);

        ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
        confirmedServiceRequest.setWrite(writeRequest);

        return confirmedServiceRequest;
    }

    private VariableAccessSpecification constructVariableAccessSpecification(FcModelNode modelNode) {
        VariableDefs listOfVariable = new VariableDefs();

        List<VariableDefs.SEQUENCE> variableDefsSeqOf = listOfVariable.getSEQUENCE();
        variableDefsSeqOf.add(modelNode.getMmsVariableDef());

        VariableAccessSpecification variableAccessSpecification = new VariableAccessSpecification();
        variableAccessSpecification.setListOfVariable(listOfVariable);

        return variableAccessSpecification;
    }

    private void decodeSetDataValuesResponse(ConfirmedServiceResponse confirmedServiceResponse)
            throws ServiceError {

        WriteResponse writeResponse = confirmedServiceResponse.getWrite();

        if (writeResponse == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "SetDataValuesResponse: improper response");
        }

        WriteResponse.CHOICE subChoice = writeResponse.getCHOICE().get(0);

        if (subChoice.getFailure() != null) {
            throw mmsDataAccessErrorToServiceError(subChoice.getFailure());
        }
    }

    /**
     * This function will get the definition of all persistent DataSets from the server and update the
     * DataSets in the ServerModel that was returned by {@code retrieveModel} or set using {@code
     * setServerModel}. It will delete DataSets that have been deleted since the last update and add
     * any new DataSets
     *
     * @throws ServiceError if a ServiceError is returned by the server.
     * @throws IOException  if a fatal association error occurs. The association object will be closed
     *                      and can no longer be used after this exception is thrown.
     */
    public void updateDataSets() throws ServiceError, IOException {

        if (serverModel == null) {
            throw new IllegalStateException(
                    "Before calling this function you have to get the ServerModel using the retrieveModel() function");
        }

        Collection<ModelNode> lds = serverModel.getChildren();

        for (ModelNode ld : lds) {
            ConfirmedServiceRequest serviceRequest =
                    constructGetDirectoryRequest(ld.getName(), "", false);
            ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
            decodeAndRetrieveDsNamesAndDefinitions(confirmedServiceResponse, (LogicalDevice) ld);
        }
    }

    private void decodeAndRetrieveDsNamesAndDefinitions(
            ConfirmedServiceResponse confirmedServiceResponse, LogicalDevice ld)
            throws ServiceError, IOException {

        if (confirmedServiceResponse.getGetNameList() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "decodeGetDataSetResponse: Error decoding server response");
        }

        GetNameListResponse getNameListResponse = confirmedServiceResponse.getGetNameList();

        List<Identifier> identifiers = getNameListResponse.getListOfIdentifier().getIdentifier();

        if (identifiers.size() == 0) {
            return;
        }

        for (Identifier identifier : identifiers) {
            // TODO delete DataSets that no longer exist
            getDataSetDirectory(identifier, ld);
        }

        if (getNameListResponse.getMoreFollows() != null
                && getNameListResponse.getMoreFollows().value == true) {
            throw new ServiceError(ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT);
        }
    }

    private void getDataSetDirectory(Identifier dsId, LogicalDevice ld)
            throws ServiceError, IOException {
        ConfirmedServiceRequest serviceRequest = constructGetDataSetDirectoryRequest(dsId, ld);
        ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
        decodeGetDataSetDirectoryResponse(confirmedServiceResponse, dsId, ld);
    }

    private ConfirmedServiceRequest constructGetDataSetDirectoryRequest(
            Identifier dsId, LogicalDevice ld) throws ServiceError {
        ObjectName.DomainSpecific domainSpecificObjectName = new ObjectName.DomainSpecific();
        domainSpecificObjectName.setDomainID(new Identifier(ld.getName().getBytes(UTF_8)));
        domainSpecificObjectName.setItemID(dsId);

        GetNamedVariableListAttributesRequest dataSetObj = new GetNamedVariableListAttributesRequest();
        dataSetObj.setDomainSpecific(domainSpecificObjectName);

        ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
        confirmedServiceRequest.setGetNamedVariableListAttributes(dataSetObj);

        return confirmedServiceRequest;
    }

    private void decodeGetDataSetDirectoryResponse(
            ConfirmedServiceResponse confirmedServiceResponse, BerVisibleString dsId, LogicalDevice ld)
            throws ServiceError {

        if (confirmedServiceResponse.getGetNamedVariableListAttributes() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "decodeGetDataSetDirectoryResponse: Error decoding server response");
        }

        GetNamedVariableListAttributesResponse getNamedVariableListAttResponse =
                confirmedServiceResponse.getGetNamedVariableListAttributes();
        boolean deletable = getNamedVariableListAttResponse.getMmsDeletable().value;
        List<VariableDefs.SEQUENCE> variables =
                getNamedVariableListAttResponse.getListOfVariable().getSEQUENCE();

        if (variables.size() == 0) {
            throw new ServiceError(
                    ServiceError.INSTANCE_NOT_AVAILABLE,
                    "decodeGetDataSetDirectoryResponse: Instance not available");
        }

        List<FcModelNode> dsMems = new ArrayList<>();

        for (VariableDefs.SEQUENCE variableDef : variables) {

            FcModelNode member;
            // TODO remove this try catch statement once all possible FCs are
            // supported
            // it is only there so that Functional Constraints such as GS will
            // be ignored and DataSet cotaining elements with these FCs are
            // ignored and not created.
            try {
                member = serverModel.getNodeFromVariableDef(variableDef);
            } catch (ServiceError e) {
                return;
            }
            if (member == null) {
                throw new ServiceError(
                        ServiceError.INSTANCE_NOT_AVAILABLE,
                        "decodeGetDataSetDirectoryResponse: data set memeber does not exist, you might have to call retrieveModel first");
            }
            dsMems.add(member);
        }

        String dsObjRef = ld.getName() + "/" + dsId.toString().replace('$', '.');

        DataSet dataSet = new DataSet(dsObjRef, dsMems, deletable);

        if (ld.getChild(dsId.toString().substring(0, dsId.toString().indexOf('$'))) == null) {
            throw new ServiceError(
                    ServiceError.INSTANCE_NOT_AVAILABLE,
                    "decodeGetDataSetDirectoryResponse: LN for returned DataSet is not available");
        }

        DataSet existingDs = serverModel.getDataSet(dsObjRef);
        if (existingDs == null) {
            serverModel.addDataSet(dataSet);
        } else if (!existingDs.isDeletable()) {
            return;
        } else {
            serverModel.removeDataSet(dsObjRef);
            serverModel.addDataSet(dataSet);
        }
    }

    /**
     * The client should create the data set first and add it to either the non-persistent list or to
     * the model. Then it should call this method for creation on the server side
     *
     * @param dataSet the data set to be created on the server side
     * @throws ServiceError if a ServiceError is returned by the server.
     * @throws IOException  if a fatal IO error occurs. The association object will be closed and can
     *                      no longer be used after this exception is thrown.
     */
    public void createDataSet(DataSet dataSet) throws ServiceError, IOException {
        ConfirmedServiceRequest serviceRequest = constructCreateDataSetRequest(dataSet);
        encodeWriteReadDecode(serviceRequest);
        handleCreateDataSetResponse(dataSet);
    }

    /**
     * dsRef = either LD/LN.DataSetName (persistent) or @DataSetname (non-persistent) Names in
     * dsMemberRef should be in the form: LD/LNName.DoName or LD/LNName.DoName.DaName
     */
    private ConfirmedServiceRequest constructCreateDataSetRequest(DataSet dataSet)
            throws ServiceError {

        VariableDefs listOfVariable = new VariableDefs();

        List<VariableDefs.SEQUENCE> variableDefs = listOfVariable.getSEQUENCE();
        for (FcModelNode dsMember : dataSet) {
            variableDefs.add(dsMember.getMmsVariableDef());
        }

        DefineNamedVariableListRequest createDSRequest = new DefineNamedVariableListRequest();
        createDSRequest.setVariableListName(dataSet.getMmsObjectName());
        createDSRequest.setListOfVariable(listOfVariable);

        ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
        confirmedServiceRequest.setDefineNamedVariableList(createDSRequest);

        return confirmedServiceRequest;
    }

    private void handleCreateDataSetResponse(DataSet dataSet) throws ServiceError {
        serverModel.addDataSet(dataSet);
    }

    public void deleteDataSet(DataSet dataSet) throws ServiceError, IOException {
        ConfirmedServiceRequest serviceRequest = constructDeleteDataSetRequest(dataSet);
        ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
        decodeDeleteDataSetResponse(confirmedServiceResponse, dataSet);
    }

    private ConfirmedServiceRequest constructDeleteDataSetRequest(DataSet dataSet)
            throws ServiceError {

        ListOfVariableListName listOfVariableListName = new ListOfVariableListName();

        List<ObjectName> objectList = listOfVariableListName.getObjectName();
        objectList.add(dataSet.getMmsObjectName());

        DeleteNamedVariableListRequest requestDeleteDS = new DeleteNamedVariableListRequest();
        requestDeleteDS.setListOfVariableListName(listOfVariableListName);

        ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
        confirmedServiceRequest.setDeleteNamedVariableList(requestDeleteDS);

        return confirmedServiceRequest;
    }

    private void decodeDeleteDataSetResponse(
            ConfirmedServiceResponse confirmedServiceResponse, DataSet dataSet) throws ServiceError {

        if (confirmedServiceResponse.getDeleteNamedVariableList() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "decodeDeleteDataSetResponse: Error decoding server response");
        }

        DeleteNamedVariableListResponse deleteNamedVariableListResponse =
                confirmedServiceResponse.getDeleteNamedVariableList();

        if (deleteNamedVariableListResponse.getNumberDeleted().intValue() != 1) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT, "number deleted not 1");
        }

        if (serverModel.removeDataSet(dataSet.getReferenceStr()) == null) {
            throw new ServiceError(ServiceError.UNKNOWN, "unable to delete dataset locally");
        }
    }

    /**
     * The implementation of the GetDataSetValues ACSI service. After a successful return, the Basic
     * Data Attributes of the data set members will contain the values read. If one of the data set
     * members could not be read, this will be indicated in the returned list. The returned list will
     * have the same size as the member list of the data set. For each member it will contain <code>
     * null</code> if reading was successful and a ServiceError if reading of this member failed.
     *
     * @param dataSet the DataSet that is to be read.
     * @return a list indicating ServiceErrors that may have occurred.
     * @throws IOException if a fatal IO error occurs. The association object will be closed and can
     *                     no longer be used after this exception is thrown.
     */
    public List<ServiceError> getDataSetValues(DataSet dataSet) throws IOException {

        ConfirmedServiceResponse confirmedServiceResponse;
        try {
            ConfirmedServiceRequest serviceRequest = constructGetDataSetValuesRequest(dataSet);
            confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
        } catch (ServiceError e) {
            int dataSetSize = dataSet.getMembers().size();
            List<ServiceError> serviceErrors = new ArrayList<>(dataSetSize);
            for (int i = 0; i < dataSetSize; i++) {
                serviceErrors.add(e);
            }
            return serviceErrors;
        }
        return decodeGetDataSetValuesResponse(confirmedServiceResponse, dataSet);
    }

    private ConfirmedServiceRequest constructGetDataSetValuesRequest(DataSet dataSet)
            throws ServiceError {

        VariableAccessSpecification varAccSpec = new VariableAccessSpecification();
        varAccSpec.setVariableListName(dataSet.getMmsObjectName());

        ReadRequest getDataSetValuesRequest = new ReadRequest();
        getDataSetValuesRequest.setSpecificationWithResult(new BerBoolean(true));
        getDataSetValuesRequest.setVariableAccessSpecification(varAccSpec);

        ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
        confirmedServiceRequest.setRead(getDataSetValuesRequest);

        return confirmedServiceRequest;
    }

    private List<ServiceError> decodeGetDataSetValuesResponse(
            ConfirmedServiceResponse confirmedServiceResponse, DataSet ds) {

        int dataSetSize = ds.getMembers().size();
        List<ServiceError> serviceErrors = new ArrayList<>(dataSetSize);

        if (confirmedServiceResponse.getRead() == null) {
            ServiceError serviceError =
                    new ServiceError(
                            ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                            "Error decoding GetDataValuesReponsePdu");
            for (int i = 0; i < dataSetSize; i++) {
                serviceErrors.add(serviceError);
            }
            return serviceErrors;
        }

        ReadResponse readResponse = confirmedServiceResponse.getRead();
        List<AccessResult> listOfAccessResults = readResponse.getListOfAccessResult().getAccessResult();

        if (listOfAccessResults.size() != ds.getMembers().size()) {
            ServiceError serviceError =
                    new ServiceError(
                            ServiceError.PARAMETER_VALUE_INAPPROPRIATE,
                            "Number of AccessResults does not match the number of DataSet members.");
            for (int i = 0; i < dataSetSize; i++) {
                serviceErrors.add(serviceError);
            }
            return serviceErrors;
        }

        Iterator<AccessResult> accessResultIterator = listOfAccessResults.iterator();

        for (FcModelNode dsMember : ds) {
            AccessResult accessResult = accessResultIterator.next();
            if (accessResult.getSuccess() != null) {
                try {
                    dsMember.setValueFromMmsDataObj(accessResult.getSuccess());
                } catch (ServiceError e) {
                    serviceErrors.add(e);
                }
                serviceErrors.add(null);
            } else {
                serviceErrors.add(mmsDataAccessErrorToServiceError(accessResult.getFailure()));
            }
        }

        return serviceErrors;
    }

    public List<ServiceError> setDataSetValues(DataSet dataSet) throws ServiceError, IOException {
        ConfirmedServiceRequest serviceRequest = constructSetDataSetValues(dataSet);
        ConfirmedServiceResponse confirmedServiceResponse = encodeWriteReadDecode(serviceRequest);
        return decodeSetDataSetValuesResponse(confirmedServiceResponse);
    }

    private ConfirmedServiceRequest constructSetDataSetValues(DataSet dataSet) throws ServiceError {
        VariableAccessSpecification varAccessSpec = new VariableAccessSpecification();
        varAccessSpec.setVariableListName(dataSet.getMmsObjectName());

        ListOfData listOfData = new ListOfData();
        List<Data> dataList = listOfData.getData();

        for (ModelNode member : dataSet) {
            dataList.add(member.getMmsDataObj());
        }

        WriteRequest writeRequest = new WriteRequest();
        writeRequest.setVariableAccessSpecification(varAccessSpec);
        writeRequest.setListOfData(listOfData);

        ConfirmedServiceRequest confirmedServiceRequest = new ConfirmedServiceRequest();
        confirmedServiceRequest.setWrite(writeRequest);

        return confirmedServiceRequest;
    }

    private List<ServiceError> decodeSetDataSetValuesResponse(
            ConfirmedServiceResponse confirmedServiceResponse) throws ServiceError {

        if (confirmedServiceResponse.getWrite() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "Error decoding SetDataSetValuesReponsePdu");
        }

        WriteResponse writeResponse = confirmedServiceResponse.getWrite();
        List<WriteResponse.CHOICE> writeResChoiceType = writeResponse.getCHOICE();
        List<ServiceError> serviceErrors = new ArrayList<>(writeResChoiceType.size());

        for (WriteResponse.CHOICE accessResult : writeResChoiceType) {
            if (accessResult.getSuccess() != null) {
                serviceErrors.add(null);
            } else {
                serviceErrors.add(mmsDataAccessErrorToServiceError(accessResult.getFailure()));
            }
        }
        return serviceErrors;
    }

    public void getRcbValues(Rcb rcb) throws ServiceError, IOException {
        getDataValues(rcb);
    }

    public void reserveUrcb(Urcb urcb) throws ServiceError, IOException {
        BdaBoolean resvBda = urcb.getResv();
        resvBda.setValue(true);
        setDataValues(resvBda);
    }

    public void reserveBrcb(Brcb brcb, short resvTime) throws ServiceError, IOException {
        BdaInt16 resvTmsBda = brcb.getResvTms();
        resvTmsBda.setValue(resvTime);
        setDataValues(resvTmsBda);
    }

    public void cancelUrcbReservation(Urcb urcb) throws ServiceError, IOException {
        BdaBoolean resvBda = urcb.getResv();
        resvBda.setValue(false);
        setDataValues(resvBda);
    }

    public void enableReporting(Rcb rcb) throws ServiceError, IOException {
        BdaBoolean rptEnaBda = rcb.getRptEna();
        rptEnaBda.setValue(true);
        setDataValues(rptEnaBda);
    }

    public void disableReporting(Rcb rcb) throws ServiceError, IOException {
        BdaBoolean rptEnaBda = rcb.getRptEna();
        rptEnaBda.setValue(false);
        setDataValues(rptEnaBda);
    }

    public void startGi(Rcb rcb) throws ServiceError, IOException {
        BdaBoolean rptGiBda = (BdaBoolean) rcb.getChild("GI");
        rptGiBda.setValue(true);
        setDataValues(rptGiBda);
    }

    /**
     * Sets the selected values of the given report control block. Note that all these parameters may
     * only be set if the RCB has been reserved but reporting has not been enabled yet.
     *
     * <p>The data set reference as it is set in an RCB must contain a dollar sign instead of a dot to
     * separate the logical node from the data set name, e.g.: 'LDevice1/LNode$DataSetName'. Therefore
     * his method will check the reference for a dot and if necessary convert it to a '$' sign before
     * sending the request to the server.
     *
     * <p>The parameters PurgeBuf, EntryId are only applicable if the given rcb is of type BRCB.
     *
     * @param rcb         the report control block
     * @param setRptId    whether to set the report ID
     * @param setDatSet   whether to set the data set
     * @param setOptFlds  whether to set the optional fields
     * @param setBufTm    whether to set the buffer time
     * @param setTrgOps   whether to set the trigger options
     * @param setIntgPd   whether to set the integrity period
     * @param setPurgeBuf whether to set purge buffer
     * @param setEntryId  whether to set the entry ID
     * @return a list indicating ServiceErrors that may have occurred.
     * @throws IOException if a fatal IO error occurs. The association object will be closed and can
     *                     no longer be used after this exception is thrown.
     */
    public List<ServiceError> setRcbValues(
            Rcb rcb,
            boolean setRptId,
            boolean setDatSet,
            boolean setOptFlds,
            boolean setBufTm,
            boolean setTrgOps,
            boolean setIntgPd,
            boolean setPurgeBuf,
            boolean setEntryId)
            throws IOException {

        List<FcModelNode> parametersToSet = new ArrayList<>(6);

        if (setRptId == true) {
            parametersToSet.add(rcb.getRptId());
        }
        if (setDatSet == true) {
            rcb.getDatSet().setValue(rcb.getDatSet().getStringValue().replace('.', '$'));
            parametersToSet.add(rcb.getDatSet());
        }
        if (setOptFlds == true) {
            parametersToSet.add(rcb.getOptFlds());
        }
        if (setBufTm == true) {
            parametersToSet.add(rcb.getBufTm());
        }
        if (setTrgOps == true) {
            parametersToSet.add(rcb.getTrgOps());
        }
        if (setIntgPd == true) {
            parametersToSet.add(rcb.getIntgPd());
        }
        if (rcb instanceof Brcb) {
            Brcb brcb = (Brcb) rcb;
            if (setPurgeBuf == true) {
                parametersToSet.add(brcb.getPurgeBuf());
            }
            if (setEntryId == true) {
                parametersToSet.add(brcb.getEntryId());
            }
        }

        List<ServiceError> serviceErrors = new ArrayList<>(parametersToSet.size());

        for (FcModelNode child : parametersToSet) {
            try {
                setDataValues(child);
                serviceErrors.add(null);
            } catch (ServiceError e) {
                serviceErrors.add(e);
            }
        }

        return serviceErrors;
    }

    private Report processReport(MMSpdu mmsPdu) throws ServiceError {

        if (mmsPdu.getUnconfirmedPDU() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "getReport: Error decoding server response");
        }

        UnconfirmedPDU unconfirmedRes = mmsPdu.getUnconfirmedPDU();

        if (unconfirmedRes.getService() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "getReport: Error decoding server response");
        }

        UnconfirmedService unconfirmedServ = unconfirmedRes.getService();

        if (unconfirmedServ.getInformationReport() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "getReport: Error decoding server response");
        }

        List<AccessResult> listRes =
                unconfirmedServ.getInformationReport().getListOfAccessResult().getAccessResult();

        int index = 0;

        if (listRes.get(index).getSuccess().getVisibleString() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "processReport: report does not contain RptID");
        }

        String rptId = listRes.get(index++).getSuccess().getVisibleString().toString();

        if (listRes.get(index).getSuccess().getBitString() == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "processReport: report does not contain OptFlds");
        }

        BdaOptFlds optFlds = new BdaOptFlds(new ObjectReference("none"), null);
        optFlds.setValue(listRes.get(index++).getSuccess().getBitString().value);

        Integer sqNum = null;
        if (optFlds.isSequenceNumber()) {
            sqNum = listRes.get(index++).getSuccess().getUnsigned().intValue();
        }

        BdaEntryTime timeOfEntry = null;
        if (optFlds.isReportTimestamp()) {
            timeOfEntry = new BdaEntryTime(new ObjectReference("none"), null, "", false, false);
            timeOfEntry.setValueFromMmsDataObj(listRes.get(index++).getSuccess());
        }

        String dataSetRef = null;
        if (optFlds.isDataSetName()) {
            dataSetRef = listRes.get(index++).getSuccess().getVisibleString().toString();
        } else {
            for (Urcb urcb : serverModel.getUrcbs()) {
                if ((urcb.getRptId() != null && urcb.getRptId().getStringValue().equals(rptId))
                        || urcb.getReference().toString().equals(rptId)) {
                    dataSetRef = urcb.getDatSet().getStringValue();
                    break;
                }
            }
            if (dataSetRef == null) {
                for (Brcb brcb : serverModel.getBrcbs()) {
                    if ((brcb.getRptId() != null && brcb.getRptId().getStringValue().equals(rptId))
                            || brcb.getReference().toString().equals(rptId)) {
                        dataSetRef = brcb.getDatSet().getStringValue();
                        break;
                    }
                }
            }
        }
        if (dataSetRef == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "unable to find RCB that matches the given RptID in the report.");
        }
        dataSetRef = dataSetRef.replace('$', '.');

        DataSet dataSet = serverModel.getDataSet(dataSetRef);
        if (dataSet == null) {
            throw new ServiceError(
                    ServiceError.FAILED_DUE_TO_COMMUNICATIONS_CONSTRAINT,
                    "unable to find data set that matches the given data set reference of the report.");
        }

        Boolean bufOvfl = null;
        if (optFlds.isBufferOverflow()) {
            bufOvfl = listRes.get(index++).getSuccess().getBool().value;
        }

        BdaOctetString entryId = null;
        if (optFlds.isEntryId()) {
            entryId = new BdaOctetString(new ObjectReference("none"), null, "", 8, false, false);
            entryId.setValue(listRes.get(index++).getSuccess().getOctetString().value);
        }

        Long confRev = null;
        if (optFlds.isConfigRevision()) {
            confRev = listRes.get(index++).getSuccess().getUnsigned().longValue();
        }

        Integer subSqNum = null;
        boolean moreSegmentsFollow = false;
        if (optFlds.isSegmentation()) {
            subSqNum = listRes.get(index++).getSuccess().getUnsigned().intValue();
            moreSegmentsFollow = listRes.get(index++).getSuccess().getBool().value;
        }

        boolean[] inclusionBitString =
                listRes.get(index++).getSuccess().getBitString().getValueAsBooleans();
        int numMembersReported = 0;
        for (boolean bit : inclusionBitString) {
            if (bit) {
                numMembersReported++;
            }
        }

        if (optFlds.isDataReference()) {
            // this is just to move the index to the right place
            // The next part will process the changes to the values
            // without the dataRefs
            index += numMembersReported;
        }

        List<FcModelNode> reportedDataSetMembers = new ArrayList<>(numMembersReported);
        int dataSetIndex = 0;
        for (FcModelNode dataSetMember : dataSet.getMembers()) {
            if (inclusionBitString[dataSetIndex]) {
                AccessResult accessRes = listRes.get(index++);
                FcModelNode dataSetMemberCopy = (FcModelNode) dataSetMember.copy();
                dataSetMemberCopy.setValueFromMmsDataObj(accessRes.getSuccess());
                reportedDataSetMembers.add(dataSetMemberCopy);
            }
            dataSetIndex++;
        }

        List<BdaReasonForInclusion> reasonCodes = null;
        if (optFlds.isReasonForInclusion()) {
            reasonCodes = new ArrayList<>(dataSet.getMembers().size());
            for (int i = 0; i < dataSet.getMembers().size(); i++) {
                if (inclusionBitString[i]) {
                    BdaReasonForInclusion reasonForInclusion = new BdaReasonForInclusion(null);
                    reasonCodes.add(reasonForInclusion);
                    byte[] reason = listRes.get(index++).getSuccess().getBitString().value;
                    reasonForInclusion.setValue(reason);
                }
            }
        }

        return new Report(
                rptId,
                sqNum,
                subSqNum,
                moreSegmentsFollow,
                dataSetRef,
                bufOvfl,
                confRev,
                timeOfEntry,
                entryId,
                inclusionBitString,
                reportedDataSetMembers,
                reasonCodes);
    }

    /**
     * Performs the Select ACSI Service of the control model on the given controllable Data Object
     * (DO). By selecting a controllable DO you can reserve it for exclusive control/operation. This
     * service is only applicable if the ctlModel Data Attribute is set to "sbo-with-normal-security"
     * (2).
     *
     * <p>The selection is canceled in one of the following events:
     *
     * <ul>
     *   <li>The "Cancel" ACSI service is issued.
     *   <li>The sboTimemout (select before operate timeout) runs out. If the given controlDataObject
     *       contains a sboTimeout Data Attribute it is possible to change the timeout after which the
     *       selection/reservation is automatically canceled by the server. Otherwise the timeout is a
     *       local issue of the server.
     *   <li>The connection to the server is closed.
     *   <li>An operate service failed because of some error
     *   <li>The sboClass is set to "operate-once" then the selection is also canceled after a
     *       successful operate service.
     * </ul>
     *
     * @param controlDataObject needs to be a controllable Data Object that contains a Data Attribute
     *                          named "SBO".
     * @return false if the selection/reservation was not successful (because it is already selected
     * by another client). Otherwise true is returned.
     * @throws ServiceError if a ServiceError is returned by the server.
     * @throws IOException  if a fatal IO error occurs. The association object will be closed and can
     *                      no longer be used after this exception is thrown.
     */
    public boolean select(FcModelNode controlDataObject) throws ServiceError, IOException {
        BdaVisibleString sbo;
        try {
            sbo = (BdaVisibleString) controlDataObject.getChild("SBO");
        } catch (Exception e) {
            throw new IllegalArgumentException(
                    "ModelNode needs to conain a child node named SBO in order to select");
        }

        getDataValues(sbo);

        return sbo.getValue().length != 0;
    }

    /**
     * Executes the Operate ACSI Service on the given controllable Data Object (DO). The following
     * subnodes of the given control DO should be set according your needs before calling this
     * function. (Note that you can probably leave most attributes with their default value):
     *
     * <ul>
     *   <li>Oper.ctlVal - has to be set to actual control value that is to be written using the
     *       operate service.
     *   <li>Oper.operTm (type: BdaTimestamp) - is an optional sub data attribute of Oper (thus it may
     *       not exist). If it exists it can be used to set the timestamp when the operation shall be
     *       performed by the server. Thus the server will delay execution of the operate command
     *       until the given date is reached. Can be set to an empty byte array (new byte[0]) or null
     *       so that the server executes the operate command immediately. This is also the default.
     *   <li>Oper.check (type: BdaCheck) is used to tell the server whether to perform the
     *       synchrocheck and interlockcheck. By default they are turned off.
     *   <li>Oper.orign - contains the two data attributes orCat (origin category, type: BdaInt8) and
     *       orIdent (origin identifier, type BdaOctetString). Origin is optionally reflected in the
     *       status Data Attribute controlDO.origin. By reading this data attribute other clients can
     *       see who executed the last operate command. The default value for orCat is 0
     *       ("not-supported") and the default value for orIdent is ""(the empty string).
     *   <li>Oper.Test (BdaBoolean) - if true this operate command is sent for test purposes only.
     *       Default is false.
     * </ul>
     * <p>
     * All other operate parameters are automatically handled by this function.
     *
     * @param controlDataObject needs to be a controllable Data Object that contains a Data Attribute
     *                          named "Oper".
     * @throws ServiceError if a ServiceError is returned by the server
     * @throws IOException  if a fatal IO error occurs. The association object will be closed and can
     *                      no longer be used after this exception is thrown.
     */
    public void operate(FcModelNode controlDataObject) throws ServiceError, IOException {
        ConstructedDataAttribute oper;
        try {
            oper = (ConstructedDataAttribute) controlDataObject.getChild("Oper");
        } catch (Exception e) {
            throw new IllegalArgumentException("ModelNode needs to conain a child node named \"Oper\".");
        }

        ((BdaInt8U) oper.getChild("ctlNum")).setValue((short) 1);
        ((BdaTimestamp) oper.getChild("T")).setInstant(Instant.now());

        setDataValues(oper);
    }

    public boolean isOpen() {
        return !closed;
    }

    public boolean getSocketIsOpen() {
        return !this.acseAssociation.getSocketIsOpen();
    }

    public boolean getSocketIsConnected() {
        return this.acseAssociation.getSocketIsConnected();
    }

    public boolean getSocketIsInputShutdown() {
        return this.acseAssociation.getSocketIsInputShutdown();
    }

    public boolean getSocketIsOutputShutdown() {
        return this.acseAssociation.getSocketIsOutputShutdown();
    }

    public boolean getSocketIsbind() {
        return this.acseAssociation.getSocketIsbind();
    }

    public boolean testConnected() {
        return this.acseAssociation.testConnected();
    }

    /**
     * Will close the connection simply by closing the TCP socket.
     */
    public void close() {
        clientReceiver.close(new IOException("Connection closed by client"));
    }

    /**
     * Will send a disconnect request first and then close the TCP socket.
     */
    public void disconnect() {
        clientReceiver.disconnect();
    }

    final class ClientReceiver extends Thread {

        private final ByteBuffer pduBuffer;
        private Integer expectedResponseId;
        private IOException lastIOException = null;

        public ClientReceiver(int maxMmsPduSize) {
            pduBuffer = ByteBuffer.allocate(maxMmsPduSize + 400);
        }

        @Override
        public void run() {
            try {
                while (true) {

                    pduBuffer.clear();
                    byte[] buffer;
                    try {
                        buffer = acseAssociation.receive(pduBuffer);
                    } catch (TimeoutException e) {
                        // Illegal state: A timeout exception was thrown.
                        throw new IllegalStateException();
                    } catch (DecodingException e) {
                        // Error decoding the OSI headers of the received packet
                        continue;
                    }

                    MMSpdu decodedResponsePdu = new MMSpdu();
                    try {
                        decodedResponsePdu.decode(new ByteArrayInputStream(buffer), null);
                    } catch (IOException e) {
                        // Error decoding the received MMS PDU
                        continue;
                    }

                    if (decodedResponsePdu.getUnconfirmedPDU() != null) {
                        if (decodedResponsePdu
                                .getUnconfirmedPDU()
                                .getService()
                                .getInformationReport()
                                .getVariableAccessSpecification()
                                .getListOfVariable()
                                != null) {
                            // Discarding LastApplError Report
                        } else {
                            if (reportListener != null) {
                                final Report report = processReport(decodedResponsePdu);

                                Thread t1 =
                                        new Thread(
                                                new Runnable() {
                                                    @Override
                                                    public void run() {
                                                        reportListener.newReport(report);
                                                    }
                                                });
                                t1.start();
                            } else {
                                // discarding report because no ReportListener was registered.
                            }
                        }
                    } else if (decodedResponsePdu.getRejectPDU() != null) {
                        synchronized (incomingResponses) {
                            if (expectedResponseId == null) {
                                // Discarding Reject MMS PDU because no listener for request was found.
                                continue;
                            } else if (decodedResponsePdu.getRejectPDU().getOriginalInvokeID().value.intValue()
                                    != expectedResponseId) {
                                // Discarding Reject MMS PDU because no listener with fitting invokeID was found.
                                continue;
                            } else {
                                try {
                                    incomingResponses.put(decodedResponsePdu);
                                } catch (InterruptedException e) {
                                    // TODO can this ever be interrupted?
                                }
                            }
                        }
                    } else if (decodedResponsePdu.getConfirmedErrorPDU() != null) {
                        synchronized (incomingResponses) {
                            if (expectedResponseId == null) {
                                // Discarding ConfirmedError MMS PDU because no listener for request was found.
                                continue;
                            } else if (decodedResponsePdu.getConfirmedErrorPDU().getInvokeID().value.intValue()
                                    != expectedResponseId) {
                                // Discarding ConfirmedError MMS PDU because no listener with fitting invokeID was
                                // found.
                                continue;
                            } else {
                                try {
                                    incomingResponses.put(decodedResponsePdu);
                                } catch (InterruptedException e) {
                                    // TODO can this ever be interrupted?
                                }
                            }
                        }
                    } else {
                        synchronized (incomingResponses) {
                            if (expectedResponseId == null) {
                                // Discarding ConfirmedResponse MMS PDU because no listener for request was found.
                                continue;
                            } else if (decodedResponsePdu.getConfirmedResponsePDU().getInvokeID().value.intValue()
                                    != expectedResponseId) {
                                // Discarding ConfirmedResponse MMS PDU because no listener with fitting invokeID
                                // was
                                // found.
                                continue;
                            } else {
                                try {
                                    incomingResponses.put(decodedResponsePdu);
                                } catch (InterruptedException e) {
                                    // TODO can this ever be interrupted?
                                }
                            }
                        }
                    }
                }
            } catch (IOException e) {
                close(e);
            } catch (Exception e) {
                close(new IOException("unexpected exception while receiving", e));
            }
        }

        public void setResponseExpected(int invokeId) {
            expectedResponseId = invokeId;
        }

        private void disconnect() {
            synchronized (this) {
                if (closed == false) {
                    closed = true;
                    acseAssociation.disconnect();
                    lastIOException = new IOException("Connection disconnected by client");
                    if (reportListener != null) {
                        Thread t1 =
                                new Thread(
                                        new Runnable() {
                                            @Override
                                            public void run() {
                                                reportListener.associationClosed(lastIOException);
                                            }
                                        });
                        t1.start();
                    }

                    MMSpdu mmsPdu = new MMSpdu();
                    mmsPdu.setConfirmedRequestPDU(new ConfirmedRequestPDU());
                    try {
                        incomingResponses.put(mmsPdu);
                    } catch (InterruptedException e1) {
                        // TODO can this ever be interrupted?
                    }
                }
            }
        }

        private void close(IOException e) {
            synchronized (this) {
                if (closed == false) {
                    closed = true;
                    acseAssociation.close();
                    lastIOException = e;
                    if (reportListener != null) {
                        Thread t1 =
                                new Thread(
                                        new Runnable() {
                                            @Override
                                            public void run() {
                                                reportListener.associationClosed(lastIOException);
                                            }
                                        });
                        t1.start();
                    }

                    MMSpdu mmsPdu = new MMSpdu();
                    mmsPdu.setConfirmedRequestPDU(new ConfirmedRequestPDU());
                    try {
                        incomingResponses.put(mmsPdu);
                    } catch (InterruptedException e1) {
                        // TODO can this ever be interrupted?
                    }
                }
            }
        }

        IOException getLastIOException() {
            return lastIOException;
        }

        MMSpdu removeExpectedResponse() {
            synchronized (incomingResponses) {
                expectedResponseId = null;
                return incomingResponses.poll();
            }
        }
    }
}
